Skip to content

fix(desktop): enforce owner-only access in internal builds - #4053

Open
brow wants to merge 14 commits into
mainfrom
internal-owner-only-enforcement
Open

fix(desktop): enforce owner-only access in internal builds#4053
brow wants to merge 14 commits into
mainfrom
internal-owner-only-enforcement

Conversation

@brow

@brow brow commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Managed agents in internal Buzz builds should answer only their owner. Previously, an agent could keep a broader access setting and respond to other people, which did not match the access policy for internal builds.

This PR makes owner-only access effective for every managed agent in internal builds and makes that restriction clear in the Desktop UI. Open source builds remain configurable.

Changes

  • Enforce owner-only access when any managed agent starts or is deployed from an internal build.
  • Show the agent access control as locked to Only me in Desktop, with an explanation of why it cannot be changed.
  • Keep Welcome teammates working under the same rule without triggering unnecessary restarts.
  • Leave open source build behavior unchanged. This changes effective runtime access without rewriting stored or relay-advertised settings.

The companion #4064 explains the restriction in-thread when someone without access mentions an agent.

The enforcement will remain inactive in shipped builds until squareup/buzz-releases#74 marks internal releases during the build.

Screenshots

Before After
Editable agent access control before the change Agent access locked to Only me in an internal build

Tests

Added coverage for:

The full Desktop Rust and JavaScript suites, type checks, formatting, clippy, and file-size checks passed. Playwright E2E was not run.


Originated from Buzz channel buzz-agent-control. Supersedes #2537.

@brow
brow marked this pull request as ready for review August 2, 2026 23:19
@brow
brow requested a review from a team as a code owner August 2, 2026 23:19
@brow
brow enabled auto-merge (squash) August 2, 2026 23:19
@brow
brow force-pushed the internal-owner-only-enforcement branch from f7af909 to 16b66e5 Compare August 2, 2026 23:50
@wesbillman

Copy link
Copy Markdown
Collaborator

Commenting on Wes Billman's behalf after a release-safety review of 16b66e5c916a68e79a7ee6b8e2be21126591c56d.

I found two blockers before this is safe to merge for an internal release:

  1. Existing provider deployments are not remediated. The owner-only projection is applied when Desktop constructs a provider deploy payload (desktop/src-tauri/src/commands/agents_deploy.rs), and that payload is sent only through an explicit provider deploy/start path (desktop/src-tauri/src/commands/agents.rs). Installing a marked Desktop build does not redeploy an agent whose backend_agent_id already exists; provider status deliberately survives Desktop restarts. Such an agent can therefore retain its previous anyone/allowlist policy while the upgraded UI reports and locks the setting to “Only me.”

    Please either add a reliable reconciliation/migration for existing provider deployments, or make enforcement state explicit and gate release on inventory plus successful redeployment. The UI must not claim the effective policy changed until the remote deployment has accepted it. Add coverage for the upgrade case: an already-deployed provider agent with wider stored/effective access receives the owner-only policy during rollout.

  2. The enforced principal set is broader than the stated contract. RespondTo::OwnerOnly in crates/buzz-acp/src/lib.rs admits the human owner and NIP-OA-verified same-owner sibling agents. This PR intentionally relies on that behavior for Welcome teammates. That may be the desired authorization model, but it conflicts with “answer only their owner” and UI copy that says only the user can send instructions.

    Please obtain an explicit product/security decision that owner ∪ verified same-owner agents is the intended internal boundary and update the PR/UI language accordingly, or change the authorization behavior to satisfy literal human-owner-only access. This is an authorization-set decision, not merely copy polish.

The local spawn clamp, independent BUZZ_ACP_ALLOWED_RESPOND_TO=owner-only guard, provider payload projection, and compile-time marker wiring otherwise look sound in the reviewed diff. Before merge, this branch should also be updated from main and revalidated after the blockers are resolved.

@brow

brow commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@wesbillman those make sense but don't seem like they should be blocking. Do you agree?

wesbillman
wesbillman previously approved these changes Aug 3, 2026
@brow
brow force-pushed the internal-owner-only-enforcement branch from 16b66e5 to 825de5a Compare August 3, 2026 19:06
npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp and others added 9 commits August 3, 2026 15:20
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Amp <amp@ampcode.com>
Ai-assisted: true
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
@brow
brow force-pushed the internal-owner-only-enforcement branch from 825de5a to f176e2d Compare August 3, 2026 22:38
npub12uu53ml9upy7ww9apmtv6vm0u8xlcldx7znsjvwgsr7uvy5g0kssw943ca and others added 2 commits August 5, 2026 08:11
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
The merge with main pushed desktop/src/features/agents/hooks.ts past the
1000-line file-size ratchet. Move the owner-only access query into a
dedicated module, which keeps hooks.ts under the limit and matches the
existing one-hook-per-file pattern next to it.

Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commenting on Wes Billman's behalf after reviewing current head 99685b52b7daa7e385eca3db84c8fb916c709de1.

Two authorization/release blockers remain:

  1. Existing provider deployments are not brought under the new policy. The owner-only projection is applied only while constructing a provider deploy payload (desktop/src-tauri/src/commands/agents_deploy.rs:144-176), and that payload reaches the provider only through create-with-deploy or an explicit Start/deploy operation (desktop/src-tauri/src/commands/agents.rs:1001-1018, 1125-1156). Existing remote infrastructure deliberately remains represented as deployed across Desktop restarts whenever backend_agent_id exists (desktop/src-tauri/src/managed_agents/runtime.rs:147-168). Upgrading to a marked build therefore leaves a previously deployed anyone/allowlist agent remotely open while the UI locks and displays “Only me.” That contradicts the PR's claim that owner-only access becomes effective for every managed agent in internal builds.

    Please reconcile already-deployed provider agents during rollout, or expose and enforce a fail-closed state until the provider has accepted an owner-only redeploy. Add an upgrade-path test covering an existing provider deployment with wider effective access.

  2. owner-only is not literally owner-only. The harness admits the human owner and every cryptographically verified same-owner sibling agent (crates/buzz-acp/src/lib.rs:220-256). This branch explicitly relies on that broader set for Welcome teammates (desktop/src/features/onboarding/welcomeGuide.ts:264-275), while the UI says “Only me” and “Only you can send instructions” (desktop/src/features/agents/ui/RespondToField.tsx:69-76,237-240). Please either obtain and document the product/security decision that the intended boundary is owner ∪ verified same-owner agents and make the user-facing contract accurate, or enforce literal human-owner-only access.

The local spawn clamp, independent BUZZ_ACP_ALLOWED_RESPOND_TO=owner-only guard, provider payload projection for new/explicit deployments, build-marker wiring, and OSS behavior otherwise look sound. CI is green at this head; I relied on the existing full Desktop checks rather than rerunning equivalent suites locally.

@wesbillman

Copy link
Copy Markdown
Collaborator

Additional blocker found in the independent security pass, commenting on Wes Billman's behalf:

  1. The baked build environment can overwrite both enforcement gates after Desktop sets them. BUZZ_BUILD_AGENT_ENV accepts arbitrary keys at build time (desktop/src-tauri/build.rs:42-71), and baked_build_env() returns those entries without reserved-key filtering (desktop/src-tauri/src/managed_agents/agent_env.rs:22-61). They enter the effective descriptor as its baked floor (desktop/src-tauri/src/managed_agents/readiness.rs:226-270). Local spawn sets BUZZ_ACP_RESPOND_TO=owner-only and BUZZ_ACP_ALLOWED_RESPOND_TO=owner-only first (desktop/src-tauri/src/managed_agents/runtime.rs:754-764), then writes every descriptor.env entry last (runtime.rs:803-813). An internal build packaged with both keys set to anyone therefore produces an agent that answers anyone.

The expanded RESERVED_ENV_KEYS protects user/persona/definition input, but not this baked tier. Reject reserved keys in BUZZ_BUILD_AGENT_ENV at build time or filter them before they reach descriptor.env, and add a test proving baked collisions cannot override either access gate. This is a direct bypass of the boundary the PR intends to make invariant.

@wesbillman

Copy link
Copy Markdown
Collaborator

One additional upgrade-path bug from Mongo's independent lifecycle review, confirmed at 99685b52b:

  1. Welcome teammate remediation never converges in owner-only builds. The new expected-state predicate requires owner-only with an empty allowlist in a marked build, but the remediation branch still unconditionally writes allowlist:[lead] (desktop/src/features/onboarding/welcomeGuide.ts:349-359). A pre-existing install whose teammates carry the old allowlist state therefore fails the predicate, writes the same invalid-for-this-build state again, and repeats on later provisioning passes. Running teammates also continue satisfying welcomeTeammateNeedsRestart (welcomeKickoff.ts:360-373), causing redundant restarts.

This is not another authorization bypass—the Rust boundary still clamps effective access—but it is a real upgrade regression and contradicts the PR's goal of avoiding unnecessary Welcome restarts. Make the remediation target owner-only/empty when the build policy is active, and test the full remediation write for a pre-existing allowlisted teammate rather than only the predicate's happy path.

npub12uu53ml9upy7ww9apmtv6vm0u8xlcldx7znsjvwgsr7uvy5g0kssw943ca and others added 3 commits August 5, 2026 09:51
`provisionWelcomeTeam` decided whether a teammate needed an access write
with `welcomeTeammateHasExpectedAccess`, which requires `owner-only` and
an empty allowlist under the owner-only-access build policy, but then
wrote `allowlist:[lead]` unconditionally. An upgraded install with
pre-existing allowlisted teammates therefore rewrote state the predicate
rejects on every provisioning pass, and `welcomeTeammateNeedsRestart`
kept restarting teammates that were already running.

Extract `welcomeTeammateAccessUpdate`, which returns the write that
satisfies the predicate for the current build (or null when the teammate
is already correct), and use it for the decision and the write so the two
cannot drift again.

Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
The baked `BUZZ_BUILD_AGENT_ENV` pairs are written into a spawned agent's
environment last, after Desktop sets the access gates and identity vars,
and nothing filtered them. A build packaged with `BUZZ_ACP_RESPOND_TO` or
`BUZZ_ACP_ALLOWED_RESPOND_TO` set to `anyone` therefore produced agents
that answer anyone while the UI shows the access locked to "Only me", so
the build capability could disable its own enforcement.

Reject reserved keys where the value enters the system, in `build.rs`, so
such a build fails instead of shipping, and keep a runtime filter in
`baked_build_env()` for a binary built without that check. The key list
is `include!`d from one source into both consumers, matching the existing
`reconnect_hook_config.rs` pattern, because a build script cannot import
from the crate and the two copies must not drift.

Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Record what the owner-only-access build capability enforces (local spawn
and every deploy payload this build serializes) and what it does not: a
provider deployment created by an unmarked build keeps its wider remote
access until it is next deployed from a marked build, even though this
build's UI shows that agent locked to "Only me".

Also state that the harness gate admits the owner and every verified
same-owner sibling agent, which is the intended boundary and what the
built-in Welcome team depends on, so a later reader does not mistake it
for a hole in the clamp.

Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Amp <amp@ampcode.com>
Ai-assisted: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants